package com.swallow.auth.infrastructure.acl.oauth2.ext;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.swallow.auth.common.constants.RedisConstants;
import com.swallow.auth.common.utils.CustomerFuture;
import com.swallow.auth.domain.auth.model.SmsType;
import com.swallow.auth.infrastructure.acl.oauth2.constants.ErrorConstants;
import com.swallow.auth.infrastructure.acl.oauth2.constants.SecurityConstants;
import com.swallow.auth.infrastructure.acl.oauth2.exception.InvalidCaptchaException;
import com.swallow.auth.infrastructure.mysql.mapper.AccountMapper;
import com.swallow.auth.infrastructure.mysql.po.AccountPO;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author: yangjie.deng@resico.cn
 * @since: 2024-05-15 11:17:17
 * @version: v1.0.0
 * @describe: 扩展登录时校验, 此处扩展短信、用户名密码登录校验
 */
@Slf4j
@Component
public class ExtDaoAuthenticationProvider extends DaoAuthenticationProvider {
    @Resource private RedisTemplate<String, Object> redisTemplate;

    private static final int ERROR_COUNT = 10;

    private static final long UNLOCK_TIME_MINUTES = 10;

    @Resource private AccountMapper accountMapper;

    public ExtDaoAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        super(passwordEncoder);
        setUserDetailsService(userDetailsService);
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
           log.info("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(super.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        // 获取当前request
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            throw new InvalidCaptchaException("Failed to get the current request.");
        }

        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);

        AtomicReference<Boolean> success = new AtomicReference<>(Boolean.TRUE);

        try {
            switch (grantType) {
                // 短信验证码登录
                case SecurityConstants.GRANT_TYPE_SMS_CODE -> smsAuthentication(authentication);
                // 用户名密码登录
                case SecurityConstants.GRANT_TYPE_USER_PWD_CODE -> userPwdAuthentication(userDetails, authentication, request);
            }
        } catch (Exception e) {
            success.set(Boolean.FALSE);
            throw e;
        } finally {
            // 修改登录时间
            CustomerFuture.runAsync(() -> afterPostProcessor(success.get(), userDetails));
        }
    }

    private void afterPostProcessor(Boolean loginResult, UserDetails userDetails) {

        if (userDetails instanceof AccountPO account) {
            AccountPO loginUpdate = new AccountPO();

            if (loginResult) {
                loginUpdate.setLoginTime(LocalDateTime.now());
                loginUpdate.setAccountLockStatus(0);
                loginUpdate.setLoginErrorCount(0);
                loginUpdate.setAccountUnlockTime(null);
            } else {
                int count = Objects.isNull(account.getLoginErrorCount()) ? 0 : account.getLoginErrorCount();
                loginUpdate.setLoginErrorCount(count + 1);

                if (Objects.nonNull(account.getLoginErrorCount()) && account.getLoginErrorCount() >= ERROR_COUNT) {
                    loginUpdate.setAccountLockStatus(1);
                    loginUpdate.setAccountUnlockTime(LocalDateTime.now().plusMinutes(UNLOCK_TIME_MINUTES));
                }
            }
            loginUpdate.setUpdateTime(LocalDateTime.now());

            accountMapper.update(loginUpdate, Wrappers.lambdaUpdate(AccountPO.class)
                                                      .eq(AccountPO::getAccountNo, account.getAccountNo()));
        }
    }

    private void userPwdAuthentication(UserDetails userDetails,
                                       UsernamePasswordAuthenticationToken authentication,
                                       HttpServletRequest request) {
        // 获取缓存中的图形验证码
        String captchaId = request.getParameter(SecurityConstants.U_PWD_CAPTCHA_ID_PARAM);

        String cacheKey = RedisConstants.PIC_CAPTCHA_KEY.formatted(captchaId);
        Object captchaCode = redisTemplate.opsForValue().get(cacheKey);

        String paramCode = request.getParameter(SecurityConstants.U_PWD_CAPTCHA_PARAM);

        // 验证码校验
        if (ObjectUtils.isEmpty(captchaCode) || !StringUtils.equalsIgnoreCase(paramCode, captchaCode.toString())) {
            throw new BadCredentialsException(ErrorConstants.ERROR_CODE);
        }
        redisTemplate.delete(cacheKey);

        // 用户名密码校验
        // 如果此处涉及到密码加密,可在此处进行解密后再调用方法
        super.additionalAuthenticationChecks(userDetails, authentication);
    }

    private void smsAuthentication(UsernamePasswordAuthenticationToken authentication) {
        String cacheKey = RedisConstants.SMS_CAPTCHA_KEY.formatted(SmsType.REGISTER_SMS_TYPE.getValue(), authentication.getPrincipal());
        Object code = redisTemplate.opsForValue().get(cacheKey);

        if (ObjectUtils.isEmpty(code) || ObjectUtils.notEqual(code, authentication.getCredentials())) {
            throw new BadCredentialsException("The sms captcha is incorrect.");
        }
        // 验证成功后,则直接将验证码删除
        redisTemplate.delete(cacheKey);
    }
}
